#
#        Python GUI - Containers - Generic
#

from Properties import overridable_property
from Exceptions import ArgumentError
from Geometry import pt_in_rect
from Components import Component
from ClipComponents import ClipComponent

class Container(ClipComponent):
    """A Container is a Component that can contain other Components.
    The sub-components are clipped to the boundary of their container."""

    contents = overridable_property('contents', "List of subcomponents. Do not modify directly.")

    #  _contents   [Component]

    def __init__(self, **kw):
        self._contents = []
        ClipComponent.__init__(self, **kw)

    def destroy(self):
        """Destroy this Container and all of its contents."""
        while self._contents:
            self._contents[-1].destroy()
        ClipComponent.destroy(self)

    #
    #        Subcomponent Management
    #
    
    def get_contents(self):
        return self._contents

    def add(self, comp):
        """Add the given Component as a subcomponent."""
        comp.container = self

    def remove(self, comp):
        """Remove subcomponent, if present."""
        if comp in self._contents:
            comp.container = None
    
    def _add(self, comp):
        # Called by comp.set_container() to implement subcomponent addition.
        self._contents.append(comp)
        if comp._tabbable:
            self._invalidate_tab_chain()
    
    def _remove(self, comp):
        # Called by comp.set_container() to implement subcomponent removal.
        self._contents.remove(comp)
        if comp._tabbable:
            self._invalidate_tab_chain()
    
    #
    #   The infamous 'place' method and friends.
    #
    
    _place_default_spacing = 8
    
    def place(self, item, 
            left = None, right = None, top = None, bottom = None,
            sticky = 'nw', scrolling = '', border = 0):
        """Add a component to the frame with positioning,
        resizing    and scrolling options. See the manual for details."""
        self._place([item], left = left, right = right, top = top, bottom = bottom,
            sticky = sticky, scrolling = scrolling, border = border)
    
    def place_row(self, items,
            left = None, right = None, top = None, bottom = None,
            sticky = 'nw', scrolling = '', border = 0, spacing = None):
        """Add a row of components to the frame with positioning,
        resizing    and scrolling options. See the manual for details."""
        if left is not None and right is not None:
            raise ValueError("Cannot specify both left and right to place_row")
        elif left is None and right is not None:
            direction = 'left'
            items = items[:]
            items.reverse()
        else:
            direction = 'right'
        self._place(items, left = left, right = right, top = top, bottom = bottom,
            sticky = sticky, scrolling = scrolling, border = border,
            direction = direction, spacing = spacing)

    def place_column(self, items,
            left = None, right = None, top = None, bottom = None,
            sticky = 'nw', scrolling = '', border = 0, spacing = None):
        """Add a column of components to the frame with positioning,
        resizing    and scrolling options. See the manual for details."""
        if top is not None and bottom is not None:
            raise ValueError("Cannot specify both top and bottom to place_column")
        elif top is None and bottom is not None:
            direction = 'up'
            items = items[:]
            items.reverse()
        else:
            direction = 'down'
        self._place(items, left = left, right = right, top = top, bottom = bottom,
            sticky = sticky, scrolling = scrolling, border = border,
            direction = direction, spacing = spacing)

    def _place(self, items, 
            left = None,
            right = None,
            top = None,
            bottom = None,
            sticky = 'nw',
            scrolling = '',
            direction = 'right',
            spacing = None,
            border = 0):
        
        def side(spec, name):
            #  Process a side specification taking the form of either
            #  (1) an offset, (2) a reference component, or (3) a
            #  tuple (component, offset). Returns a tuple (ref, offset)
            #  where ref is the reference component or None (representing
            #  the Frame being placed into). Checks that the reference
            #  component, if any, is directly contained by this Frame.
            ref = None
            offset = None
            if spec is not None:
                if isinstance(spec, tuple):
                    ref, offset = spec
                elif isinstance(spec, Component):
                    ref = spec
                    offset = 0
                elif isinstance(spec, (int, float)):
                    offset = spec
                else:
                    raise ArgumentError(self, 'place', name, spec)
            if ref is self:
                ref = None
            elif ref:
                con = ref.container
                #if con is not self and isinstance(con, ScrollFrame):
                #    ref = con
                #    con = ref.container
                if con is not self:
                    raise ValueError("Reference component for place() is not"
                        " directly contained by the frame being placed into.")
            return ref, offset
        
        if spacing is None:
            spacing = self._place_default_spacing
        
        # Decode the sticky options
        hmove = vmove = hstretch = vstretch = 0
        if 'e' in sticky:
            if 'w' in sticky:
                hstretch = 1
            else:
                hmove = 1
        if 's' in sticky:
            if 'n' in sticky:
                vstretch = 1
            else:
                vmove = 1
        
        # Translate the direction argument
        try:
            dir = {'right':0, 'down':1, 'left':2, 'up':3}[direction]
        except KeyError:
            raise ArgumentError(self, 'place', 'direction', direction)
            
        # Unpack the side arguments
        left_obj, left_off = side(left, 'left')
        right_obj, right_off = side(right, 'right')
        top_obj, top_off = side(top, 'top')
        bottom_obj, bottom_off = side(bottom, 'bottom')
        
        # Process the items
        #if not isinstance(items, list):
        #    items = [items]
        for item in items:
            x, y = item.position
            w, h = item.size
            # Calculate left edge position
            if left_obj:
                l = left_obj.left + left_obj.width + left_off
            elif left_off is not None:
                if left_off < 0:
                    l = self.width + left_off
                else:
                    l = left_off
            else:
                l = None
            # Calculate top edge position
            if top_obj:
                t = top_obj.top + top_obj.height + top_off
            elif top_off is not None:
                if top_off < 0:
                    t = self.height + top_off
                else:
                    t = top_off
            else:
                t = None
            # Calculate right edge position
            if right_obj:
                r = right_obj.left + right_off
            elif right_off is not None:
                if right_off <= 0:
                    r = self.width + right_off
                else:
                    r = right_off
            else:
                r = None
            # Calculate bottom edge position
            if bottom_obj:
                b = bottom_obj.top + bottom_off
            elif bottom_off is not None:
                if bottom_off <= 0:
                    b = self.height + bottom_off
                else:
                    b = bottom_off
            else:
                b = None
            # Fill in unspecified positions
            if l is None:
                if r is not None:
                    l = r - w
                else:
                    l = x
            if r is None:
                r = l + w
            if t is None:
                if b is not None:
                    t = b - h
                else:
                    t = y
            if b is None:
                b = t + h
            if scrolling:
                item.scrolling = scrolling
            # Position, resize and add the item
            item.resize(bounds = (l, t, r, b))
            self.add(item)
            # Record resizing and border options
            item.hmove = hmove
            item.vmove = vmove
            item.hstretch = hstretch
            item.vstretch = vstretch
            item.border = border
            # Step to the next item
            if dir == 0:
                left_obj = item
                left_off = spacing
            elif dir == 1:
                top_obj = item
                top_off = spacing
            elif dir == 2:
                right_obj = item
                right_off = -spacing
            else:
                bottom_obj = item
                bottom_off = -spacing

    #
    #        Resizing
    #

    def resized(self, delta):
        for c in self._contents:
            c.container_resized(delta)
    
    #
    #   Tabbing
    #
    
    def _build_tab_chain(self, chain):
        ClipComponent._build_tab_chain(self, chain)
        for c in self._contents:
            c._build_tab_chain(chain)

    #
    #   Other
    #
    
    def shrink_wrap(self, padding = (0, 0)):
        """Adjust the size of the component so that it just encloses its
        contents, plus the specified padding on the right and bottom."""
        contents = self.contents
        rights = [item.right for item in contents]
        bottoms = [item.bottom for item in contents]
        hpad, vpad = padding
        self.size = (max(rights) + hpad, max(bottoms) + vpad)
    
    def broadcast(self, message, *args):
        """Traverse the component hierarchy, calling each component's handler for
        the given message, if any."""
        ClipComponent.broadcast(self, message, *args)
        for comp in self._contents:
            comp.broadcast(message, *args)
